iT邦幫忙

2022 iThome 鐵人賽

DAY 28
0
Software Development

Oops! OOPP: An Introduction to Object-Oriented Programming in Python系列 第 28

「形式型別」故意和「實際型別」不同,作用何在?

  • 分享至 

  • xImage
  •  

今天補充昨天一些未交代細節。


「多型」Polymorphism架構圖

  • 下面是筆者的手繪圖示。很簡陋,但勉強能說明多型的大致架構和分類:
    https://ithelp.ithome.com.tw/upload/images/20221013/20148485qDYycZm5wH.png
  • 請記住:本章所談的「多型」,是上圖右邊的「執行時期」這塊。筆者並未著墨於左邊的「編譯時期」多型。原因之前也有解釋:不論method overloading或operator overloading,Python都沒有支援(註1)。

「形式型別」和「實際型別」

  • 昨天講到,實作多型時,通常是宣告父類別的型別,但實際new出的是子類別物件,例如Tree hardwood = new Hardwood();。其中Tree是「形式類別」(下改稱「形式型別」)(註2),亦即父類別;Hardwood為「實際類別」(下稱「實際型別」),也就是子類別。

  • 形式型別和實際型別不同,這樣的設計究竟有甚麼好處?「形實一致」可能帶來哪些不便?

  • 其實昨天已部分回答了這個問題。現在進一步說明:

    • 形式型別是在編譯時期(compile-time)就決定,而實際型別則是執行時期(run-time)的事。
    • 在判斷某個物件有哪些方法可用時,編譯器看的是在編譯期間已確定的形式型別,完全不管實際型別,要管也管不著。
    • 於是一旦Tree hardwood = new Hardwood();,hardwood物件就只能執行Tree類別已宣告的方法,通常子類別都會覆寫override這些方法。而Tree沒有宣告的方法,hardwood一律不能呼叫,一呼叫就報錯。
    • 多型機制如此規範,目的之一是將形式型別視為規格(或介面)製定者,後代子孫必須按規格行事,不可逾越。
  • 另外一個好處是:這樣設計系統比較容易擴充。

  • 假設下列情形:

    • 昨天的C#程式,設計完成運作一陣子後,如果需求有變,必須新增一個TreeDoctor類別。這個樹醫生類別醫術非常高明,甚麼樣的樹生病他都能治。不管是闊葉樹、針葉樹,甚至金星樹、火星樹、土星樹都難不倒這位神醫。一言以蔽之,只要是「樹」(Tree),他就能治病。
    • 這位樹醫生有一個cure()方法,接受一個Tree型別的參數。所以這個方法的signature是public void cure(Tree tree)(註3)。
  • 新增「樹醫生類別」的程式如下。為求精簡,刪去一些不相干的code:

    using System;
    namespace MyApplication
    {
      class Tree  // Base class (parent)
      {
      }
    
      class Hardwood : Tree  // Derived class (child)
      {
      }
    
      class Conifer : Tree  // Derived class (child)
      {
      }
    
      class VenusianTree : Tree   // 金星樹
      {
      }
    
      class MartianTree : Tree   // 火星樹
      {
      }
    
      class SaturnianTree : Tree   // 土星樹
      {
      }
    
      class TreeDoctor    // 新增這個「樹醫生」類別。
      {
        public void cure(Tree tree)  // 只要是「樹」我就能治。
        {
          Console.WriteLine("I am curing the " + tree + '.');
        }
      }
    
      class Program
      {
        static void Main(string[] args)
        {
          Tree hardwood = new Hardwood();            // 「形式型別」是Tree,「實際型別」是Hardwood。
          Tree conifer = new Conifer();              // 「形式型別」是Tree,「實際型別」是Conifer。
          Tree venusianTree = new VenusianTree();    // 「形式型別」是Tree,「實際型別」是VenusianTree。
          Tree martianTree = new MartianTree();      // 「形式型別」是Tree,「實際型別」是MartianTree。
          Tree saturnianTree = new SaturnianTree();  // 「形式型別」是Tree,「實際型別」是SaturnianTree。
    
          TreeDoctor doctor = new TreeDoctor();
    
          // 樹醫生要去醫治不同的樹了:
          doctor.cure(hardwood);         
          doctor.cure(conifer);
          doctor.cure(venusianTree);
          doctor.cure(martianTree);
          doctor.cure(saturnianTree);
        }
      }
    }    
    
  • 程式正確執行,樹醫生坐太空船到各星球出診去了:
    https://ithelp.ithome.com.tw/upload/images/20221013/201484856tswZhuszc.png

  • 但是...如果樹的物件「形式型別」和「實際型別」設為一致:

    using System;
    namespace MyApplication
    {
      class Tree  // Base class (parent)
      {
      }
    
      class Hardwood : Tree  // Derived class (child)
      {
      }
    
      class Conifer : Tree  // Derived class (child)
      {
      }
    
      class VenusianTree : Tree   // 金星樹
      {
      }
    
      class MartianTree : Tree   // 火星樹
      {
      }
    
      class SaturnianTree : Tree   // 土星樹
      {
      }
    
      class TreeDoctor    // // 新增這個「樹醫生」類別。
      {
        public void cure(Tree tree)  // 我要的參數是「通用型的樹」,而不是某個特定樹種。
        {
          Console.WriteLine("I am curing the " + tree + '.');
        }
      }
    
      class Program
      {
        static void Main(string[] args)
        {
          hardwood hardwood = new Hardwood();                 // 「形式型別」和「實際型別」都是Hardwood。
          Conifer conifer = new Conifer();                    // 「形式型別」和「實際型別」都是Conifer。
          VenusianTree venusianTreeTree = new VenusianTree(); // 「形式型別」和「實際型別」都是VenusianTree。
          MartianTree martianTree = new MartianTree();        // 「形式型別」和「實際型別」都是MartianTree。
          SaturnianTree saturnianTree = new SaturnianTree();  // 「形式型別」和「實際型別」都是SaturnianTree。
    
          TreeDoctor doctor = new TreeDoctor();
    
          // 樹醫生要去醫治不同的樹了。但,可以嗎?
          doctor.cure(hardwood);
          doctor.cure(conifer);
          doctor.cure(venusianTree);
          doctor.cure(martianTree);
          doctor.cure(saturnianTree);
        }
      }
    }
    
  • 樹醫生的cure()方法,參數的型別必須為「通用的樹」,而不是某個「特定樹種」,主程式卻傳了一棵特定樹種型別的樹給他。這樣會引發錯誤:
    https://ithelp.ithome.com.tw/upload/images/20221013/20148485vpiHtLcYNY.png

  • 這是Tree hardwood = new Hardwood();寫法好處之一。當然還有其他優點,就恕不一一枚舉了。

多型的限制

  • 以下是筆者搜集到的一些多型的限制(註4):
    • 多型機制只能用在方法(函數),不能用在屬性(attributes)。所以一講多型,指的必然是方法,屬性根本談不上多不多型。
    • 也不是所有種類的方法都可以產生多型。多型只用在「實例方法」(instance methods),至於「類別方法」(class methods),是無法運用多型的。這個限制其實也很自然,因為多型只作用於實例(instance),不作用於整個類別。別忘了類別是抽象的「模板」或「藍圖」,並無實物,所以根本和多型扯不上半毛錢關係。只有實例才可能會造成形式型別和實際型別不一致而形成多型

連續兩天用C#講多型,似乎有點偏離本系列題目OO Programming in Python。好,明天就介紹 Python的多型吧。


註1: 之前筆者也有提及,看過有人用些奇奇怪怪的方法去實作出Python的method overloading。基於實用性不大,過程又有點複雜,除非有心「練功」,否則不看也罷。

註2: 請參考本系列Day 2: 類別是「模板」或「藍圖」。該文有說明類別就是自訂的型別

註3: signature一般不包括傳回值。這裡為了完整表達整個方法的定義,特地將其傳回值void也納入。

註4: 這些限制是筆者從Java的資料找到,不確定是否為通例。


上一篇
Polymorphism「現形」記
下一篇
Polymorphism in Python
系列文
Oops! OOPP: An Introduction to Object-Oriented Programming in Python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言